Qdrant: A Beginner's Tutorial to Vector Search

Welcome to this tutorial on Qdrant, an open-source vector database and vector similarity search engine. Qdrant helps you build the next generation of AI-powered applications by providing a powerful, production-ready service to store, search, and manage vector embeddings.

In this notebook, we will cover the fundamental concepts of Qdrant and walk through a practical example of building a simple semantic search engine. We'll explore:

  1. Setup and Installation: Getting your environment ready.
  2. Connecting to Qdrant: Initializing the Qdrant client.
  3. Creating Collections: Setting up a space for our vectors.
  4. Generating and Uploading Vectors: Preparing and storing our vector data.
  5. Performing Searches: Running semantic and filtered searches.

1. Setup and Installation

First, you'll need to install the necessary Python libraries. We'll use qdrant-client to interact with Qdrant, sentence-transformers to generate our vector embeddings, and python-dotenv to manage our API keys and environment variables.

You can install them by running the following command in your terminal:

#pip install qdrant-client sentence-transformers python-dotenv -q

Managing Environment Variables

It's a best practice to manage sensitive information like API keys securely. We'll use a .env file to store these credentials. Create a file named .env in the same directory as this notebook and add the following lines:

QDRANT_API_KEY="your_qdrant_api_key_here"

QDRANT_URL="your_qdrant_url_here"

For this tutorial, you can easily set up a Qdrant cloud cluster and get the above env variables.

import os
from dotenv import load_dotenv
from qdrant_client import QdrantClient, models
from sentence_transformers import SentenceTransformer

# Load environment variables from .env file
load_dotenv()

# Get the API key and URL from environment variables
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
QDRANT_URL = os.getenv("QDRANT_URL")

2. Connecting to Qdrant

The QdrantClient is our entry point to interacting with the Qdrant service. Qdrant can be run in different ways:

  • In-memory: Perfect for quick experiments and testing, as all data is cleared when the process finishes.
  • On-disk storage: A local, persistent storage option.
  • Docker: Running Qdrant as a standalone server.
  • Qdrant Cloud: A fully managed, scalable cloud solution.

For simplicity, we'll use the Qdrant cloud in this tutorial.

# Initialize the Qdrant client for in-memory storage
# client = QdrantClient(":memory:")

# If you were connecting to Qdrant Cloud, you would use:
client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)

3. Creating a Collection

In Qdrant, a collection is a named set of points (vectors with a payload). Think of it as a table in a SQL database. When creating a collection, you need to define the configuration for the vectors it will store, such as their size (dimensionality) and the distance metric for similarity search.

Common distance metrics include:

  • Cosine: Measures the angle between two vectors. Great for text embeddings.
  • Euclidean: The straight-line distance between two points.
  • Dot Product: A measure of similarity that also considers vector magnitude.
my_collection = "my_first_collection"

client.recreate_collection(
    collection_name=my_collection,
    vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE),
)
/var/folders/pv/g_b0j0n53rz5fm8yrlw3jg040000gn/T/ipykernel_48560/1209000986.py:3: DeprecationWarning: `recreate_collection` method is deprecated and will be removed in the future. Use `collection_exists` to check collection existence and `create_collection` instead.
  client.recreate_collection(
True

4. Generating and Uploading Vectors

To perform semantic search, we need to convert our text data into numerical representations called vector embeddings. We'll use a pre-trained model from sentence-transformers for this task. The model all-MiniLM-L6-v2 is a good choice for its balance of speed and quality, and it outputs vectors of size 384, matching our collection's configuration.

# Load a pre-trained sentence transformer model
model = SentenceTransformer('all-MiniLM-L6-v2')

# Let's create some sample documents
documents = [
    {"id": 1, "text": "Qdrant is a vector database for building AI applications.", "metadata": {"type": "tech"}},
    {"id": 2, "text": "The Eiffel Tower is a famous landmark in Paris, France.", "metadata": {"type": "travel"}},
    {"id": 3, "text": "A vector database indexes vectors for easy search and retrieval.", "metadata": {"type": "tech"}},
    {"id": 4, "text": "The Great Wall of China is one of the world's wonders.", "metadata": {"type": "travel"}},
    {"id": 5, "text": "Artificial intelligence is transforming many industries.", "metadata": {"type": "tech"}}
]

# Generate embeddings for our documents
embeddings = model.encode([doc["text"] for doc in documents])
/Users/devon/.pyenv/versions/3.10.14/lib/python3.10/site-packages/torch/nn/modules/module.py:1520: FutureWarning: `encoder_attention_mask` is deprecated and will be removed in version 4.55.0 for `BertSdpaSelfAttention.forward`.
  return forward_call(*args, **kwargs)

Upserting Points

Now that we have our embeddings, we'll upload them to our Qdrant collection. In Qdrant, a point is the central entity, consisting of a vector, a unique ID, and an optional payload (a JSON object for metadata). We use the upsert operation, which will add new points or update existing ones with the same ID.

client.upsert(
    collection_name=my_collection,
    points=[
        models.PointStruct(
            id=doc["id"],
            vector=embedding.tolist(),
            payload=doc["metadata"]
        )
        for doc, embedding in zip(documents, embeddings)
    ],
    wait=True,
)
UpdateResult(operation_id=0, status=<UpdateStatus.COMPLETED: 'completed'>)

5. Performing Searches

Semantic Search

The core functionality of a vector database is finding the most similar vectors to a given query vector. This is semantic search. We'll take a query, encode it into a vector using the same model, and then use Qdrant to find the closest matches.

query = "What is a vector database?"
query_vector = model.encode(query).tolist()

search_results = client.query_points(
    collection_name=my_collection,
    query=query_vector,
    limit=2,  # Return the top 2 most similar results
    with_payload=True,
)

# Map IDs to original document text so we can display it with results
id_to_text = {doc["id"]: doc["text"] for doc in documents}

print("Semantic Search Results:")
for result in search_results.points:
    print(f"- ID: {result.id}, Score: {result.score:.4f}")
    text = id_to_text.get(result.id)
    if text is not None:
        print(f"  Text: {text}")
Semantic Search Results:
- ID: 3, Score: 0.6311
  Text: A vector database indexes vectors for easy search and retrieval.
- ID: 1, Score: 0.5301
  Text: Qdrant is a vector database for building AI applications.

Search with a Filter

Qdrant's real power comes from its ability to combine vector search with filtering on the payload. This allows you to narrow down your search to only the points that match certain metadata criteria. Here, we'll search for the same query but only within documents of the "travel" type.

# Create a payload index for the string field 'type' so we can filter on it
# Qdrant requires an index of type KEYWORD for exact-match string filters
try:
    client.create_payload_index(
        collection_name=my_collection,
        field_name="type",
        field_schema=models.PayloadSchemaType.KEYWORD,
    )
    print("Created payload index for 'type' (KEYWORD).")
except Exception as e:
    # Safe to ignore if already exists and we're re-running cells
    if "already exists" in str(e).lower():
        print("Payload index for 'type' already exists.")
    else:
        raise
Created payload index for 'type' (KEYWORD).
filtered_search_results = client.query_points(
    collection_name=my_collection,
    query=query_vector,
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="type",
                match=models.MatchValue(value="travel")
            )
        ]
    ),
    limit=2,
    with_payload=True,
)

print("Filtered Search Results (type='travel'):")
for result in filtered_search_results.points:
    print(f"- ID: {result.id}, Score: {result.score:.4f}, Payload: {result.payload}")
    text = id_to_text.get(result.id)
    if text is not None:
        print(f"  Text: {text}")
Filtered Search Results (type='travel'):
- ID: 2, Score: 0.0032, Payload: {'type': 'travel'}
  Text: The Eiffel Tower is a famous landmark in Paris, France.
- ID: 4, Score: -0.0419, Payload: {'type': 'travel'}
  Text: The Great Wall of China is one of the world's wonders.

Conclusion

Congratulations! You've successfully built a small semantic search engine using Qdrant. You've learned how to:

  • Set up your environment and connect to Qdrant.
  • Create collections with specific vector configurations.
  • Generate vector embeddings from text data.
  • Upsert points with vectors and metadata payloads.
  • Perform both semantic and filtered searches.

This is just the beginning. From here, you can explore more advanced topics like:

  • Hybrid Search: Combining keyword-based (sparse) and semantic (dense) vectors for more accurate results.
  • Scalability: Using Docker or Qdrant Cloud for larger, production-level applications.
  • Advanced Filtering: Creating more complex filtering conditions.

Happy building!